// Giocondo Asynchronous JavaScript and XML (Ajax) and DOM library.
// Really, without XML :-)

// Do not use globals JavaScript variables or functions because they can
// collide with users' JavaScript code. All the code should be wrapped into one
// object called Giocondo which acts as a namespace. This is the only
// identifier added to the global namespace and the only that can cause a
// clash, so the probability a name clashing will occur is greatly reduced.

// We implemente the singleton pattern to ensure that the object used for
// asynchronous communication could only be instantiated once and that an
// instance is only created when it's actually needed.
// See http://www.kaijaeger.com/articles/the-singleton-design-pattern-in-javascript.html
// TODO: see also http://www.phpied.com/3-ways-to-define-a-javascript-class/
var Giocondo = (function() {

	var instance = null;

	function privateConstructor() {
		// Properties.

		// Lookup table to convert event codes. Server-side PHP transmits
		// event codes instead of JavaScript event name strings to optimize.
		this.eventName = ['onclick', 'onchange'];

		// Events are queued to avoid overlapping and sending of requests faster than
		// they can be retrieved. Requests are serialized but sent asynchronously that
		// is JavaScript doesn't wait for them. This assures simultaneous events won't
		// corrupt widgets' state saved on the server session by returning to old
		// states. A subsequent event is only sent when the all the previous events are
		// completed.
		this.eventQueue = [];

		// Current event.
		this.event = null;

		// Console window to display server errors.
		this.consoleWin = null;

		// Maps widget ids to the window they belong.
		// Index number 0 is special: it corresponds to the hide box; there is only
		// one hide box and it is located under the first window.
		// TODO: what about windows opened by windows? 
		this.win = [window];

		this.request = function() {
			var ro = null;
			if (window.XMLHttpRequest) {
				ro = new XMLHttpRequest();
			} else if (window.ActiveXObject) {
				// TODO: IE versions < 7.0 do not support the standard object
				// XMLHttpRequest; support for them should be removed in the far
				// future.
				ro = new ActiveXObject('Microsoft.XMLHTTP');
			}
			if (ro == null) {
				alert("Client error: your browser doesn't support AJAX.\nThis site won't function properly.\nPlease try a better browser.");
			}
			return ro;
		}();

		// Methods.

		// TODO: if we run out of alpha characters replace one letter named functions
		// with a single dispatcher function which dentifies the function to call by a
		// numeric argument. If we don't run out, at the end of development consider
		// renaming all one letter named functions to achieve maximum consistency and
		// readability.

		// Ajax and event dispatcher.

		// Send/queue Ajax request to handle a widget event.
		this.h = function(id, e, v) {
			// TODO: ajax mantiene connessione http, permette send multipli?
			if (e == 1 && typeof v == 'undefined') {
				v = this.w(id).value;
			}

			if (this.event) {
				// onChanged event are costly due to the presence of the payload, that is
				// the value of the widget to which the event refers at the time is it
				// triggered.  To avoid using too much memory on the event queue
				// consecutive pending onChanged events are queued as a single event (the
				// last one, to ensure the state of the object on server reflects the last
				// state it has on client).  As a consequence quick edits over slow lines
				// will be send asynchronously and piecewise (like multiple paste
				// operations), not character by character as they were typed.
				if (e == 1 && this.eventQueue.length &&
						this.eventQueue[0][0] == id && this.eventQueue[0][1] == e) {
					this.eventQueue[0][2] = v;
				} else {
					// If you prefer push and shift could also
					// be used to manipulate the event queue.
					this.eventQueue.unshift(e == 1 ? [id, e, v] : [id, e]);
				}
			} else {
				this.event = e == 1 ? [id, e, v] : [id, e];
				// Here we always use the post method not the get for many reasons:
				//
				// We do not want to have variables (which may contain sensitive data)
				// recorded in web server log files;
				//
				// GET should be used when querying information, POST
				// when sending information to the server or configuring forms;
				//
				// A client and/or server may limit the size of the HTTP request header
				// allowed (e.g. see LimitRequestFieldSize Apache directive which is 8190
				// bytes by default); PHP by default limits POST size to 8M (see
				// post_max_size directive in php.ini).
				//
				// Some browsers (e.g. IE) caches the result of HTTP GET requests. The
				// POST method ensure that the event is sent to the server.
				//
				// TODO: Ajax caching problem affects POST?. session_cache_limiter('nocache')
				// used on the server is not enough for some browsers configured to do
				// aggressive cachingi (e.g. IE)? See here for implementing a workaround to
				// this client-side:
				// http://www.spacebug.com/solving_browser_caching_problem_of_ajax.html
				// http://webreflection.blogspot.com/2006/12/stupid-ajax-cache-problem-solution.html
				this.request.open('post', location.href);
				this.request.setRequestHeader('Content-Type',
						'application/x-www-form-urlencoded');
				var me = this;
				this.request.onreadystatechange = function() { me.handleResponse(); };
				this.request.send('id=' + id + '&e=' + e +
						(e == 1 ? '&v=' + encodeURIComponent(v) : ''));
			}
		}

		// Strip line breaks from PHP generated errors.  The regular expression
		// used will need to be changed if PHP HTML error format changes.
		this.stripBreaks = function(error) {
			var breaks = new RegExp('^<br />\\n(.+)<br />\\n$');
			var match = breaks.exec(error);
			if (match != null) {
				return match[1];
			}
			// Matching failed, return error message unchanged.
			return error;
		}

		this.handleResponse = function() {
			with (this) {
				if (request.readyState != 4) {
					return;
				}

				if (request.status != 200) {
					var status = request.status;
					if (confirm('Server error' + (status != 0 ? ': ' + status + ' '
							+ request.statusText : '') +
							'\n\nWould you like to abandon this application?')) {
						document.write('Application interrupted due to server error. You may ' +
							'<a href="javascript:location.reload(true)">try to reload it</a>.');
					} else {
						// Retry sending event.
						h(event[0], event[1], event[2]);
					}
					return;
				}

				var response = request.responseText;
				// TODO: if the event requires a not empty response a server error
				// similar as above should be emitted. But how can we know if a non
				// empty response is required?
				if (response != '') {
					// DEBUG
					alert(response);
					// If the response isn't a sequence of JavaScript function calls
					// (with one letter lowercase function names) it is considered an
					// error and sent to the client-side console.
					var code = new RegExp('^[a-z]\\(');
					if (code.test(response)) {
						// TODO: catch errors in eval
						eval(response);
					} else {
						// Errors sent this way (not using this.s) are not handled by
						// Giocondo server side custom error handled. They are sent
						// directly by PHP on stdout (if PHP configuration directive
						// display_errors is on).
						this.s(stripBreaks(response));
					}
				}

				event = null;
				if (eventQueue.length) {
					// Fire up next event waiting in queue.
					var newEvent = eventQueue.pop();
					h(newEvent[0], newEvent[1], newEvent[2]);
				}
			} // with
		}

		// DOM library.

		// Cast a number to an array of one element.
		this.arrayCast = function(value) {
			if (typeof value == 'number') {
				return [value];
			}
			return value;
		}

		// Add a server-side error message to a console-like window.
		this.s = function(error) {
			// Clients that supports Ajax should support window.open
			// thus no method detection "if (window.open)" is done.
			if (! this.consoleWin || this.consoleWin.closed) {
				// TODO: scrollbar=auto is standard? Are there other useful options to set?
				// see http://msdn2.microsoft.com/en-us/library/ms536651.aspx 
				// http://developer.mozilla.org/en/docs/DOM:window.open
				// http://www.infimum.dk/HTML/JSwindows.html
				consoleWin = window.open('', 'Giocondo_ErrorConsole', 
						'width=600,height=200,directories=no,location=no,menubar=no,scrollbars=yes,status=no,toolbar=no');
				consoleWin.document.title = 'Giocondo Server Error Console';
			} else {
				consoleWin.focus();
			}

			consoleWin.document.write(error + '<br />');
			// TODO: clear the history so that users can't go back
			// TODO: auto scroll to the end so that last error is always visible
		}

		// TODO: if there is a browser supporting ajax but not supporting getElementById
		// use this variant. What about getElementByTagName used below?
		// getElementByID is W3C DOM standard, support for browsers that
		// implement only document.all should be removed in the future.
		// Note that the test is used to define the function and not performed at
		// every call within it. This allows us to make the test only one time and
		// have an optimized function to use.
		//if (document.getElementById) {
		//	this.w = function(id) {
		//		return document.getElementById('w' + id);
		//	}
		//} else if (document.all) {
		//	this.w = function(id) {
		//		return document.all['w' + id];
		//	}
		//}

		// Get widget id.
		this.w = function(id) {
			return this.win[id].document.getElementById('w' + id);
		}

		// Set a widget text.
		this.t = function(id, value) {
			// Shorter way, but not standard: this.w(id).innerHTML = value;
			// TODO: Test it. what about percent strings?
			this.w(id).firstChild.nodeValue = value;
		}

		// Set a box label.
		this.l = function(id, value) {
			// TODO: test it.
			this.w(id).getElementsByTagName('LEGEND').item(0).nodeValue = value;
		}

		// Set a window title.
		this.d = function(value) {
			document.title = value;
		}

		// Hook a widget event handler.
		this.e = function(id, event) {
			// Degrade in older browser with no errors.
			if (this.request == null) return;

			var widget = this.w(id);
			// Convert event code to name
			var name = this.eventName[event];

			// Instead of object.property === undefined we could write
			// typeof object.property == 'undefined' for properly property detection.
			// Note that widgets are all of type Element so tagName can also be used
			// instead of nodeName.
			if (event == 1 && widget.nodeName == 'INPUT') {
			  if (widget.ontextinput !== undefined) {
					// TODO: Standard ontextinput event. Will have a broad support in the
					// future.
					name = 'ontextinput';
				} else if (widget.oninput !== undefined) {
					// TODO: oninput is not detected this way in Firefox and needs to be
					// setted with addEventListener (there's no oninput property).  In the
					// future this bug should be fixed. Opera works, but fires the event
					// twice for each change sometimes.  Search in
					// http://www.dannyg.com/javascript/quickref/index.html and fire up a
					// bug in Bugzilla.
					name = 'oninput';
				}	else if (widget.onpropertychange !== undefined) {
					name = 'onpropertychange';
					// TODO: add to the function below js code to check that the value
					// changes and test it with IE.
				}
			}

			// Here an anonymous function (closure) must be used, otherwise this.h(id,
			// event) will be called and its result will be assigned. Also we can't use
			// this.h because h will be called without arguments and without an object
			// and this will be bound to the special JavaScript global object (window in
			// web browsers). We use me.h which equals this when the callback is created.
			// And, in contrast to this, me will keep referring to the correct Giocondo
			// object.
			var me = this;
			this.w(id)[name] = function() { me.h(id, event); };
			// Another way to attach the event that allows attaching more than one
			// callback to an event. But we don't need this so we use the simpler way.
			//var me = this;
			//this.w(id).addEventListener(name, function() { me.h(id, event); }, false);
		}

		// Clear a widget event handler.
		this.n = function(id, event) {
			this.w(id)[this.eventName[event]] = null;
			// Another way: removes only this event among multiple attached. But we don't
			// need this so we use the simpler way.
			//this.w(id).removeEventListener(this.eventName[event], function() { me.h(id, event); }, false);
		}

		// Add a piece of HTML code in the hide box.
		this.a = function(id, value) {
			// Note that "document.write" or equivalent statements cannot be used
			// because in intrinsic event handlers it creates and writes to a new
			// document rather than modifying the current one.
			// TODO: innerHTML is not standard but only widely implemented and its
			// use cannot be avoided here without breaking simplicity.  Is there or
			// will be a better standard alternative?
			this.w(0).innerHTML += value;
			this.win[id] = this.win[0];
			this.m(id);
		}

		// Set child or children of an widget.
		this.c = function(id, childIds, orientation) {
			// Move all existing children from the parent element to a hidden widget box.

			var child, childId, parent = this.w(id), hidebox = this.w(0);
			for (var i = 0; child = parent.childNodes[i]; i++) {
				// TODO: see note about getting attribute values (search getNamedItem).
				if (child.id != '') {
					hidebox.appendChild(child);

					// Get rid of the initial m character in id.
					childId = child.id.substr(1);	// also substring or slice will do: TODO: what's better supported?

					this.win[childId] = this.win[0];
					this.m(childId);
				}
			}

			// Place new children inside the element.

			// For a single widget ignore the undefined orientation argument (will be
			// false)
			// TODO: or should I say "will be undefined and thus false"?
			childIds = this.arrayCast(childIds);
			for (var i in childIds) {
				child = this.w(childIds[i]);
				if (i != 0 && orientation) {
					parent.appendChild(document.createElement('br'));
				}
				parent.appendChild(child);
				// TODO: see note about getting attribute values (search getNamedItem).
				childId = child.id.substr(1);	// also substring or slice will do
				this.win[childId] = this.win[id];
				this.m(childId);
			}
		}

  	// Opens a new window.
  	this.o = function(id, html, defaultWidth, defaultHeight) {
			// This also registers the new window.
			// TODO: see http://library.gnome.org/devel/gtk/stable/GtkWindow.html
			// If the window's "natural" size (its size request) is larger than the 
			// default, the default will be ignored. Setting a default size of 
			// -1 means to use the "natural" default size (the size request of the 
			// window). Can I use newWin.body.width or newWin.innerWidth/outerWidth
			// to find out natural size?
			defaultWidth = typeof defaultWidth == 'undefined' ? 200 : defaultWidth;
			defaultHeight = typeof defaultHeight == 'undefined' ? 200 : defaultHeight;
			var newWin = this.win[id] = window.open('', 'Giocondo_w' + id,
				'width=' + defaultWidth + ',height=' + defaultHeight +
				',directories=no,location=no,menubar=no,scrollbars=yes,status=no,toolbar=no');
			// TODO: DEBUG
			//alert(html);
			newWin.document.write(html);
			newWin.document.close();

			// We could setup an onload event here to make the work of this.m,
			// but in Opera 9 this doesn't work, because of the interference of the
			// builtin popup blocker.  We'll let the created window to call us back.
			// This also seems to be more consistent with the asynchronous philosophy:
			// don't call me baby, I'll call you.
		}

		// Sets all widget descending from (that is contained in) a newly created
		// Giocondo window as belonging to the corresponding JavaScript window.
		// Called asynchronously when the loading of the new window is completed.
		this.m = function(id) {
			var newWin = this.win[id];
			// Register first window.
			if (newWin == undefined) newWin = this.win[id] = window;
			my = this;
			(function (w) {
				// TODO: the DOM level 0 simpler way to get an attribute value
				// (e.g. w.id) has been deprecated by W3C in DOM level 1.
				// w.attributes.getNamedItem('id').nodeValue should be used instead (a
				// check that w.attributes is defined must be done first). See here:
				// http://www.w3.org/TR/1998/REC-DOM-Level-1-19981001/level-one-html.html#ID-642250288
				// TODO: check URL
				if (w.id != '') {
					var wId = new RegExp('^w([0-9]+)$');
					var match = wId.exec(w.id);
					if (match != null) {
						my.win[match[1]] = newWin;
					}
				}
		
				for (var i = 0, child; child = w.childNodes[i]; i++) {
					// Recursive call of an anonymous function.
					arguments.callee(child);
				}
			})(newWin.document.getElementById('w' + id));
		}

		// This is the opposite of this.m.
		// TODO: consider merging them using a boolen parameter.
		this.unregister = function(id) {
			my = this;
			(function (w) {
				// TODO: See note about geNamedItem above.
				if (w.id != '') {
					var wId = new RegExp('^w([0-9]+)$');
					var match = wId.exec(w.id);
					if (match != null) {
						delete my.win[match[1]];
					}
				}
		
				for (var i = 0, child; child = w.childNodes[i]; i++) {
					// Recursive call of an anonymous function.
					arguments.callee(child);
				}
			})(w(id));
		}

		// Delete a widget
		this.f = function(id) {
			var w = w(id);

			// TODO: see note about getting attribute values (search getNamedItem).
			if (this.win[id] == id) {
				// Close a window.
				var win = this.win[id];
				win.close();

				for (var i in this.win) {
					if (this.win[id] == win) {
						delete this.win[id];
					}
				}
			} else {
				// Destroy a widget.

				this.unregister(id);

				// A typical way to delete an element since there is no removeElement
				// DOM method: you need to grab a reference of the element, find
				// it's parent, then remove the element reference.
				w.parentNode.removeChild(w);
			}
		}
	}

	return new function() {
		this.getInstance = function() {
			if (instance == null) {
				instance = new privateConstructor();
				instance.constructor = null;
			}
			return instance;
		}
  }
})();

// End of file
// vim: set ts=2 sw=2 noet:

